<?php

/**
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

/**
 * LoggerAppenderPDO appender logs to a database using the PHP's PDO extension.
 *
 * ## Configurable parameters: ##
 *
 * - dsn - The Data Source Name (DSN) used to connect to the database.
 * - user - Username used to connect to the database.
 * - password - Password used to connect to the database.
 * - table - Name of the table to which log entries are be inserted.
 * - insertSQL - Sets the insert statement for a logging event. Defaults
 * to the correct one - change only if you are sure what you are doing.
 * - insertPattern - The conversion pattern to use in conjuction with insert
 * SQL. Must contain the same number of comma separated
 * conversion patterns as there are question marks in the
 * insertSQL.
 *
 * @version $Revision: 1374546 $
 * @package log4php
 * @subpackage appenders
 * @since 2.0
 * @license http://www.apache.org/licenses/LICENSE-2.0 Apache License, Version
 *          2.0
 * @link http://logging.apache.org/log4php/docs/appenders/pdo.html Appender
 *       documentation
 */
class LoggerAppenderPDO extends LoggerAppender
{
    
    // ******************************************
    // *** Configurable parameters ***
    // ******************************************
    
    /**
     * DSN string used to connect to the database.
     *
     * @see http://www.php.net/manual/en/pdo.construct.php
     */
    protected $dsn;

    /**
     * Database user name.
     */
    protected $user;

    /**
     * Database password.
     */
    protected $password;

    /**
     * The insert query.
     *
     * The __TABLE__ placeholder will be replaced by the table name from
     * {@link $table}.
     *
     * The questionmarks are part of the prepared statement, and they must
     * match the number of conversion specifiers in {@link insertPattern}.
     */
    protected $insertSQL = "INSERT INTO __TABLE__ (timestamp, logger, level, message, thread, file, line) VALUES (?, ?, ?, ?, ?, ?, ?)";

    /**
     * A comma separated list of {@link LoggerPatternLayout} format strings
     * which replace the "?" in {@link $insertSQL}.
     *
     * Must contain the same number of comma separated conversion patterns as
     * there are question marks in {@link insertSQL}.
     *
     * @see LoggerPatternLayout For conversion patterns.
     */
    protected $insertPattern = "%date{Y-m-d H:i:s},%logger,%level,%message,%pid,%file,%line";

    /**
     * Name of the table to which to append log events.
     */
    protected $table = 'log4php_log';

    /**
     * The number of recconect attempts to make on failed append.
     */
    protected $reconnectAttempts = 3;
    
    // ******************************************
    // *** Private memebers ***
    // ******************************************
    
    /**
     * The PDO instance.
     *
     * @var PDO
     */
    protected $db;

    /**
     * Prepared statement for the insert query.
     *
     * @var PDOStatement
     */
    protected $preparedInsert;

    /**
     * This appender does not require a layout.
     */
    protected $requiresLayout = false;
    
    // ******************************************
    // *** Appender methods ***
    // ******************************************
    
    /**
     * Acquires a database connection based on parameters.
     * Parses the insert pattern to create a chain of converters which will be
     * used in forming query parameters from logging events.
     */
    public function activateOptions()
    {
        try {
            $this->establishConnection();
        } catch (PDOException $e) {
            $this->warn("Failed connecting to database. Closing appender. Error: " . $e->getMessage());
            $this->close();
            return;
        }
        
        // Parse the insert patterns; pattern parts are comma delimited
        $pieces = explode(',', $this->insertPattern);
        $converterMap = LoggerLayoutPattern::getDefaultConverterMap();
        foreach ($pieces as $pattern) {
            $parser = new LoggerPatternParser($pattern, $converterMap);
            $this->converters[] = $parser->parse();
        }
        
        $this->closed = false;
    }

    /**
     * Connects to the database, and prepares the insert query.
     *
     * @throws PDOException If connect or prepare fails.
     */
    protected function establishConnection()
    {
        // Acquire database connection
        $this->db = new PDO($this->dsn, $this->user, $this->password);
        $this->db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
        
        // Prepare the insert statement
        $insertSQL = str_replace('__TABLE__', $this->table, $this->insertSQL);
        $this->preparedInsert = $this->db->prepare($insertSQL);
    }

    /**
     * Appends a new event to the database.
     *
     * If writing to database fails, it will retry by re-establishing the
     * connection up to $reconnectAttempts times. If writing still fails,
     * the appender will close.
     */
    public function append(LoggerLoggingEvent $event)
    {
        for ($attempt = 1; $attempt <= $this->reconnectAttempts + 1; $attempt ++) {
            try {
                // Attempt to write to database
                $this->preparedInsert->execute($this->format($event));
                $this->preparedInsert->closeCursor();
                break;
            } catch (PDOException $e) {
                $this->warn("Failed writing to database: " . $e->getMessage());
                
                // Close the appender if it's the last attempt
                if ($attempt > $this->reconnectAttempts) {
                    $this->warn("Failed writing to database after {$this->reconnectAttempts} reconnect attempts. Closing appender.");
                    $this->close();
                    // Otherwise reconnect and try to write again
                } else {
                    $this->warn("Attempting a reconnect (attempt $attempt of {$this->reconnectAttempts}).");
                    $this->establishConnection();
                }
            }
        }
    }

    /**
     * Converts the logging event to a series of database parameters by using
     * the converter chain which was set up on activation.
     */
    protected function format(LoggerLoggingEvent $event)
    {
        $params = array();
        foreach ($this->converters as $converter) {
            $buffer = '';
            while ($converter !== null) {
                $converter->format($buffer, $event);
                $converter = $converter->next;
            }
            $params[] = $buffer;
        }
        return $params;
    }

    /**
     * Closes the connection to the logging database
     */
    public function close()
    {
        // Close the connection (if any)
        $this->db = null;
        
        // Close the appender
        $this->closed = true;
    }
    
    // ******************************************
    // *** Accessor methods ***
    // ******************************************
    
    /**
     * Returns the active database handle or null if not established.
     *
     * @return PDO
     */
    public function getDatabaseHandle()
    {
        return $this->db;
    }

    /**
     * Sets the username.
     */
    public function setUser($user)
    {
        $this->setString('user', $user);
    }

    /**
     * Returns the username.
     */
    public function getUser($user)
    {
        return $this->user;
    }

    /**
     * Sets the password.
     */
    public function setPassword($password)
    {
        $this->setString('password', $password);
    }

    /**
     * Returns the password.
     */
    public function getPassword($password)
    {
        return $this->password;
    }

    /**
     * Sets the insert SQL.
     */
    public function setInsertSQL($sql)
    {
        $this->setString('insertSQL', $sql);
    }

    /**
     * Returns the insert SQL.
     */
    public function getInsertSQL($sql)
    {
        return $this->insertSQL;
    }

    /**
     * Sets the insert pattern.
     */
    public function setInsertPattern($pattern)
    {
        $this->setString('insertPattern', $pattern);
    }

    /**
     * Returns the insert pattern.
     */
    public function getInsertPattern($pattern)
    {
        return $this->insertPattern;
    }

    /**
     * Sets the table name.
     */
    public function setTable($table)
    {
        $this->setString('table', $table);
    }

    /**
     * Returns the table name.
     */
    public function getTable($table)
    {
        return $this->table;
    }

    /**
     * Sets the DSN string.
     */
    public function setDSN($dsn)
    {
        $this->setString('dsn', $dsn);
    }

    /**
     * Returns the DSN string.
     */
    public function getDSN($dsn)
    {
        return $this->setString('dsn', $dsn);
    }
}
